package com.ayu.shortlink.admin.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.ayu.shortlink.admin.common.convention.exception.ClientException;
import com.ayu.shortlink.admin.common.convention.exception.ServiceException;
import com.ayu.shortlink.admin.common.convention.result.Result;
import com.ayu.shortlink.admin.common.convention.result.Results;
import com.ayu.shortlink.admin.dao.entity.FileDO;
import com.ayu.shortlink.admin.dao.mapper.FileMapper;
import com.ayu.shortlink.admin.dto.req.FileUploadReqDTO;
import com.ayu.shortlink.admin.dto.resp.FileUploadRespDTO;
import com.ayu.shortlink.admin.service.FileService;
import com.ayu.shortlink.admin.util.MinioUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.messages.Part;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

/**
 * 文件服务实现类
 */
@Service
@Slf4j
public class FileServiceImpl extends ServiceImpl<FileMapper, FileDO> implements FileService, CommandLineRunner {
    @Value("${minio.bucketName}")
    private String bucketName;
    @Value("${minio.endpoint}")
    private String endpoint;
    @Resource
    private MinioUtil minioUtil;
    @Autowired
    private FileMapper fileMapper;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private Redisson redisson;
    @Override
    public void run(String... args) throws Exception {
        // TODO 判断当前的bucket是否已经创建 同时得避免多服务器同时启动 对他进行创建操作(分布式锁+双重判定)
        BucketExistsArgs ExistsArgs = BucketExistsArgs.builder()
                .bucket(bucketName)
                .build();
        try {
            CompletableFuture<Boolean> future = minioUtil.bucketExists(ExistsArgs);

            RLock lock = redisson.getLock("INIT_MINIO_BUCKET_LOCK");

            lock.lock();
            try {
                // 如果不存在 则进行创建
                if (Boolean.FALSE.equals(future.get())){
                    future = minioUtil.bucketExists(ExistsArgs);
                    //双重判定
                    if (Boolean.FALSE.equals(future.get())){
                        minioUtil.makeBucket(MakeBucketArgs.builder()
                                .bucket(bucketName)
                                .build());
                    }
                }
            }finally {
                lock.unlock();
            }
        }catch (Exception e){
            throw new ClientException("初始化Minio桶异常，请检查");
        }
    }

    /**
     * @param requestParam
     * @return
     */
    @Override
    public Result<FileUploadRespDTO> initMultipartUpload(FileUploadReqDTO requestParam) {
        String md5 = requestParam.getMd5();
        // 根据md5去Redis查找是否有相应的FileUploadReqDTO
        String jsonString = stringRedisTemplate.opsForValue().get(md5);
        String object;
        // 如果Redis有 以Redis为准
        if (StrUtil.isNotBlank(jsonString)){
            requestParam = JSON.parseObject(jsonString, FileUploadReqDTO.class);
            object = requestParam.getObject();
        }else {
            // 如果Redis没有 证明是第一次上传
            // 需要对该文件进行命名
            String suffix = FileUtil.extName(requestParam.getOriginFileName());
            String fileName = FileUtil.mainName(requestParam.getOriginFileName());

            // 重命名 日期 + 原来的文件名 + MD5 + 后缀类型
            String date = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd");

            object = date + "/" + fileName + "_" + requestParam.getMd5() + "." + suffix;
            requestParam.setObject(object);
            requestParam.setType(suffix);
        }

        FileUploadRespDTO response;
        // 表示单片上传
        if (requestParam.getChunkCount() == 1){
            log.info("进行单片上传");
            response = minioUtil.singleUpload(requestParam.getContentType(),object);
        }else {
            // 表示分片上传
            log.info("进行分片上传");
            response = minioUtil.multiPartUpload(requestParam, object);
        }
        // 放至Redis 方便断续断传
        stringRedisTemplate.opsForValue().set(requestParam.getMd5(), JSON.toJSONString(requestParam),30,TimeUnit.MINUTES);
        return Results.success(response);
    }

    /**
     * 使用MD5查找文件是否存在
     * @param md5
     * @return
     */
    @Override
    @Transactional
    public Result<FileUploadReqDTO> queryByMD5(String md5) {
        String jsonString = stringRedisTemplate.opsForValue().get(md5);
        FileUploadReqDTO response = null;
        if (StrUtil.isNotBlank(jsonString)){
            response = JSON.parseObject(jsonString, FileUploadReqDTO.class);
            // 看是否是断点续传
            List<Part> parts = minioUtil.getParts(response.getObject(), response.getUploadId());
            List<Integer> listParts = parts.stream()
                    .map(Part::partNumber)
                    .toList();
            response.setListParts(listParts);
        }
        // 如果没有的话 就去DB查
        if (response == null){
            LambdaQueryWrapper<FileDO> queryWrapper = Wrappers.lambdaQuery(FileDO.class)
                    .eq(FileDO::getMd5, md5);
            FileDO fileDO = fileMapper.selectOne(queryWrapper);

            response = BeanUtil.toBean(fileDO, FileUploadReqDTO.class);
        }
        return Results.success(response);
    }

    @SneakyThrows
    @Override
    @Transactional
    public Result<String> mergeMultipartUpload(String md5) {
        String jsonString = stringRedisTemplate.opsForValue().get(md5);
        if (StrUtil.isBlank(jsonString)){
            throw new ServiceException("合并失败");
        }
        FileUploadReqDTO fileUploadReqDTO = JSON.parseObject(jsonString, FileUploadReqDTO.class);

        FileDO fileDO = BeanUtil.toBean(fileUploadReqDTO, FileDO.class);
        // 拼接URL
        String url = StrUtil.format("{}/{}/{}", endpoint, bucketName, fileUploadReqDTO.getObject());

        fileDO.setUrl(url);
        fileDO.setBucket(bucketName);

        // 进行拼接
        List<Part> partsList = minioUtil.getParts(fileUploadReqDTO.getObject(), fileUploadReqDTO.getUploadId());
        Part[] parts = new Part[partsList.size()];
        int partNumber = 1;
        for (Part part : partsList) {
            parts[partNumber - 1] = new Part(partNumber, part.etag());
            partNumber++;
        }
        // 分片数量大于1 需要进行合并
        if (fileUploadReqDTO.getChunkCount() > 1){
            try {
                minioUtil.mergeMultipartUpload(fileUploadReqDTO.getObject(), fileUploadReqDTO.getUploadId());
            }catch (Exception exception){
                throw new ServiceException("合并失败");
            }
        }
        fileMapper.insert(fileDO);
        stringRedisTemplate.delete(md5);
        return Results.success(url);
    }

}
